<?PHP
/*
 * $Id: barge-lucene.classes.php 25 2009-05-09 10:46:07Z namba@craid.co.jp $
 * =============================================================================
 * Barge - Web application framework on PHP5 
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * =============================================================================
 * For more information, see http://barge.googlecode.com/
 */
namespace barge\lucene;

use \Exception;

use barge\System;
use barge\io\FileInputStream;
use barge\io\FileOutputStream;
use barge\utils\ArrayList;
use barge\utils\ArrayUtils;
use barge\utils\Comparator;
use barge\utils\HashMap;
use barge\utils\Math;
use barge\utils\String;
use barge\logging\LogFactory;


/**
 * @version 1.0 2009/04/02
 * @since Barge 1.3 2009/04/04 
 */
class IndexWriter 
{
	/**
	 * ロガー
	 * @var Logger 
	 */
	protected $log;
	
	/**
	 * @var stirng 
	 */
	protected $dir;
	
	/**
	 * @var FileOutputStream
	 */
	protected $datOut;
	
	/**
	 * @var FileOutputStream
	 */
	protected $invOut;
	
	/**
	 * @var FileOutputStream
	 */
	protected $posOut;
	
	/**
	 * @var FileOutputStream
	 */
	protected $preOut;
	
	/**
	 * @var FileOutputStream
	 */
	protected $fldOut;

	protected $invcache = array();
	
	/**
	 * Field cache 
	 * @var array
	 */
	protected $fldcache = array();
	protected $fldseq = 1;
	
	/**
	 * Latest document id.
	 * @var int 
	 */
	protected $latestDocId = 1;
	
	/**
	 * Tokenizer
	 * @var Analyzer
	 */
	protected $analyzer;
	
	/**
	 * Lengh of prefix index,
	 * @var int
	 */
	protected $prefixDepth = 3;
	
	/**
	 * @var int
	 */
	protected $maxDoc = 0;
	
	/**
	 * @param string $dir インデックスディレクトリ
	 * @param Analyzer $analyzer トークン解析用のアナライザ
	 */
	public function __construct($dir, Analyzer $analyzer)
	{
		$this->dir = $dir;
		$this->analyzer = $analyzer;
		$this->log = LogFactory::getLogger();
		$this->datOut = new FileOutputStream($this->dir.'/.dat');
		$this->invOut = new FileOutputStream($this->dir.'/.inv');
		$this->fldOut = new FileOutputStream($this->dir.'/.fld');
		$this->posOut = new FileOutputStream($this->dir.'/.pos');
		$this->preOut = new FileOutputStream($this->dir.'/.pre');
		
		$this->posOut->seek(9);
	}
	
	/**
	 * @since 1.0 2009/04/03
	 */
	public function close()
	{
		$this->datOut->close();
		$this->invOut->close();
		$this->fldOut->close();
		$this->posOut->close();
		$this->preOut->close();
	}

	/**
	 * @since 1.0 2009/04/03
	 */
	public function commit()
	{
		$this->log->debug('Commit ...');
		$this->log->debug('Update indexes ...');
		
		// 転置インデックスの書き込み
		$this->updateIndexes();
		
		$this->log->debug('Update field info ...');
		
		// フィールド情報の更新
		$this->updateFieldInfo();
		
		// ドキュメントの総数を書き込み	
		$this->posOut->seek(0);
		$this->posOut->write(String::sprintf("%08d\n", $this->maxDoc));
	}
	
	/**
	 * フィールド情報の更新
	 */
	protected function updateFieldInfo()
	{
		// フィールド情報を書き込み	
		foreach ($this->fldcache as $name => $seq){
			$this->fldOut->write(sprintf("%03d%s\n", $seq, $name));
		}
	}
	
	/**
	 * 転置インデックスの更新
	 */
	protected function updateIndexes()
	{
		$this->log->debug('Sorting ...');
		ksort($this->invcache, SORT_STRING);
		
		// - ターム数
		$this->invOut->write('00000000\n');
		
		// - 単語数から拡張領域を計算	
		$extendPos = 9 + (count($this->invcache) * 169);
		$normalPos = 9;
		$termCount = 0;
//		$prefix = null;
		foreach ($this->invcache as $term => $fields) {
			ksort($fields, SORT_NUMERIC);
			foreach ($fields as $fieldseq => $ids) {
				// 転置インデックスを書き込み	
				$idcnt = $freq = count($ids);
				$this->invOut->seek($normalPos);
				if ($idcnt > 0) {
					while ($idcnt > 0) {
	
						// Term:29 Field no:3
//						String::rightPad($term, 29, ' ')
						$this->invOut->write(str_pad($term, 29, ' ').sprintf("%03d", $fieldseq));
						// Freq
						$this->invOut->write(String::sprintf("%08d", $freq));
						for ($i = 0; $i < 5; $i++) {
							if ($idcnt > 0) {
		//						// トークン数から比重を計算	
		//						$lengthNorm = (1 / sqrt($ids[$idcnt - 1]['tokens']));
								// Docuemnt Id, tf, position
								$this->invOut->write(sprintf("%08d%08d%08d", 
									$ids[$idcnt - 1]['id'], $ids[$idcnt - 1]['tokens'], $ids[$idcnt - 1]['position']));
							} else {
								$this->invOut->write(sprintf("%08d%08d%08d", 0, 0, 0));
							}
							$idcnt--;
						}
						$this->invOut->write(($idcnt > 0 ? $extendPos : "00000000"));
						$this->invOut->write("\n");
						// 次の拡張領域を探す	
						if ($idcnt > 0) {
							$this->invOut->seek($extendPos);
							$extendPos+=169;
						}
					}
				} else {
					// Term:29 Field no:3
					$this->invOut->write(str_pad($term, 29, ' ').sprintf("%03d", $fieldseq));
					$this->invOut->write(sprintf("%08d", 0));
					for ($i = 0; $i < 5; $i++) 
						$this->invOut->write(sprintf("%08d%08d%08d", 0, 0, 0));
					$this->invOut->write("00000000");
					$this->invOut->write("\n");
				}

				$termCount++;
				$normalPos += 169;
			}

		}
		
		$this->log->debug('Total '.$termCount.' terms');
		
		// 先頭にターム数を書き込み
		$this->invOut->seek(0);
		$this->invOut->write(sprintf("%08d\n", $termCount));
	}
	
	
	private $datpos;

	/**
	 * インデックスにドキュメントを追加する	
	 * @param Document $doc
	 */
	public function addDocument(Document $doc)
	{
		$buff = null;
		
		$docId = $this->nextDocumentId();
		foreach ($doc->getFields() as $field) {
			/* @var $field Field */
			$fldseq = 0;
			if (isset($this->fldcache[$field->getName()])) {
				$fldseq = $this->fldcache[$field->getName()];
			} else {
				$this->fldcache[$field->getName()] = $this->fldseq;
				$fldseq = $this->fldseq++;
			}
			
			// インデックスの作成
			if ($field->isIndexed()) {
				$tokens = array();
				// TOKENIZED の場合はアナライザーによる解析	
				if ($field->isTokenized()) {
					$tokens = $this->analyzer->analyze($field->getValue());
				} else {
					$value = $field->getValue();
					$tokens = new ArrayList(new Token($value, 0, mb_strlen($value) - 1));
				}
//				// 項目の総数
//				$tokenNum = count($tokens);
				// 項目すべてをキャッシュ	
				foreach ($tokens as $token) {
					/* @var $token Token */
					$tokentxt = $token->getText();
					if (strlen($tokentxt) > 29 || $tokentxt === '') {
						$this->log->warn("Skip invalidate token : {$tokentxt}");
						continue; 
					}
//					$key = str_pad($tokentxt, 29, ' ').sprintf("%03d", $fldseq);
					if (!isset($this->invcache[$tokentxt])) {
						$this->invcache[$tokentxt] = array();
					}
					if (!isset($this->invcache[$tokentxt][$fldseq])) {
						$this->invcache[$tokentxt][$fldseq] = array();
					}
						 
					$this->invcache[$tokentxt][$fldseq][] = array(
						'id' => $docId,
//						'field' => $fldseq,
						'position' => $token->getPosition(),
						'tokens' => count($tokens)
						);
					
//					// プレフィックスの作成
//					$len = String::length($tokentxt);	
//					for ($i = 1; $i < $len; $i++) {
//						$prefix = String::substr($tokentxt, 0, $i);
//						if (!isset($this->invcache[$prefix])) {
//							$this->invcache[$prefix] = array();
//						}
//						if (!isset($this->invcache[$prefix][$fldseq])) {
//							$this->invcache[$prefix][$fldseq] = array();
//						}
//					}
				}
			}
			// データ用バッファー	
			$buff .= String::sprintf("%03d%08d%s", $fldseq, strlen($field->getValue()), $field->getValue());
		}
		
		$this->posOut->write(String::sprintf("%08d%08d\n", $docId, $this->datpos));
		$this->datOut->write(String::sprintf("%08d%08d%s\n", $docId, strlen($buff), $buff));
		$this->datpos = $this->datOut->tell();
		
		// ドキュメント数を加算
		$this->maxDoc++;
	}
	
	/**
	 * Retuns next document id.	
	 * @return int
	 */
	protected function nextDocumentId()
	{
		return $this->latestDocId++;
	}
}

/**
 * ターム
 * @version 1.0 2009/04/04
 * @since barge 1.2 2009/04/04
 */
class Term
{
	/**
	 * @var string 
	 */
	private $field;
	
	/**
	 * @var string 
	 */
	private $text;
	
	/**
	 * @param string $field
	 * @param string $text
	 */
	public function __construct($field, $text)
	{
		$this->field = $field;
		$this->text = $text;
	}
	
	/**
	 * タームテキストを返す	
	 * @return string
	 */
	public function getText() 
	{
		return $this->text;
	}
	
	/**
	 * フィールド名を返します	
	 * @return string
	 */
	public function getField() 
	{
		return $this->field;
	}
}

/**
 * フィールド情報
 * @version 1.0 2009/04/04
 * @since Barge 1.2 2009/04/04
 */
class Field
{
	/**
	 * フィールド名
	 * @var string 
	 */
	private $name;
	
	/**
	 * @var bool
	 */
	private $indexed = false;
	
	/**
	 * @var bool
	 */
	private $tokenized = false;
	
	/**
	 * @var string
	 */
	private $value;
	
	/**
	 * @param string $name フィールド名
	 * @param string $value フィールド値
	 * @param bool $indexed インデックスを行うかどうか 
	 * @param bool $tokenized {@link Analyzer}による トークン解析を行うかどうか
	 */
	public function __construct($name, $value, $indexed = false, $tokenized = false)
	{
		$this->name = $name;
		$this->value = (string)$value;
		$this->indexed = $indexed;
		$this->tokenized = $tokenized;
	}
	
	/**
	 * @return string 
	 */
	public function getName()
	{
		return $this->name;
	}
	
	/**
	 * @return string 
	 */
	public function getValue()
	{
		return $this->value;
	}
	
	/**
	 * @return bool
	 */
	public function isIndexed()
	{
		return $this->indexed;
	}
	
	/**
	 * @return bool
	 */
	public function isTokenized()
	{
		return $this->tokenized;
	}
}

/**
 * フィールド情報
 * @version 1.0 2009/04/04
 * @since Barge 1.2 2009/04/04
 */
class FieldsInfo
{
	/**
	 * @var array
	 */
	private $fldindex = array();

	/**
	 * @var array
	 */
	private $fldindex2 = array();
	
	/**
	 * @param string $dir
	 */
	public function __construct($dir)
	{
		$fp = fopen($dir. '/.fld', 'r');
		while ($s = fgets($fp)) {
			$this->fldindex[trim(substr($s, 3))] = intval(substr($s, 0, 3));
			$this->fldindex2[intval(substr($s, 0, 3))] = trim(substr($s, 3));
		}
		fclose($fp);
	}
	
	/**
	 * @param int $fieldNo
	 * @throws Exception 
	 */
	public function getFieldName($fieldNo)
	{
		if (isset($this->fldindex2[$fieldNo])) {
			return $this->fldindex2[$fieldNo];
		} else {
			$message = "Undefined field no {$fieldNo}";
			throw new Exception($message);
		}
	}

	/**
	 * @param string $fieldName
	 * @throws Exception 
	 */
	public function getFieldNo($fieldName)
	{
		if (isset($this->fldindex[$fieldName])) {
			return $this->fldindex[$fieldName];
		} else {
			$message = "未定義のフィールド '{$fieldName}' が指定されました";
			throw new Exception($message);
		}
	}
	
	/**
	 * @param int $fieldNo
	 * @throws Exception 
	 */
	public function close()
	{
	}
}

/**
 * ドキュメント	
 * 
 * @version 1.0 2009/04/02
 */
class Document
{
	/**
	 * @var ArrayList
	 */
	private $fields;
	
	/**
	 * @var HashMap
	 */
	private $fieldNames;

	/**
	 * 
	 */
	public function __construct()
	{
		$this->fields = new ArrayList();
		$this->fieldNames = new HashMap();
	}
	
	/**
	 * ドキュメントにフィールドを追加する
	 * @param Field $field
	 */
	public function addField(Field $field)
	{
		$this->fields->add($field);
		$this->fieldNames->put($field->getName(), $field);
	}

	/**
	 * フィールド名の列挙を返す			
	 * @return ArrayList<string>
	 */
	public function getFieldNames()
	{
		return $this->fieldNames->keys();
	}
	
	/**
	 * フィールド名を指定してフィールド値を取得する
	 * @param string $name フィールド名		
	 * @return string 
	 */
	public function getFieldValue($name)
	{
		if ($this->fieldNames->contains($name)) {
			return $this->fieldNames->get($name)->getValue();
		} else {
			return null;
		}
	}
	
	/**
	 * @return ArrayList
	 */
	public function getFields()
	{
		return $this->fields;
	}
}

/** 
 * 単語にインデックスされたドキュメント
 * @version 1.1 2009/05/06
 * @since barge 1.3 2009/04/03
 */
interface TermDocs 
{
	/**
	 * @return int
	 */
	public function getId();
	
	/**
	 * 対象ドキュメントのフィールドに含まれるタームの数を返す	
	 * @return int
	 */
	public function termCount();
	
	/**
	 * @return bool
	 */
	public function next();

}

/**
 * {@link TermDocs}のデフォルト実装クラス	
 * 
 * @version 1.0 2009/04/03
 * @since Barge 1.3 2009/04/03
 * @see TermDocs
 */
class IndexTermDocs implements TermDocs
{	
	/**
	 * @var ArrayList
	 */
	private $docs;
	
	/**
	 * @var ArrayList
	 */
	private $numTerms;
	
	/**
	 * @var int
	 */
	private $index = -1;
	
	/**
     * @param ArrayList $docs
     * @param ArrayList $numTerms
	 */
	public function __construct(ArrayList $docs, ArrayList $numTerms)
	{
		$this->docs = $docs;
		$this->numTerms = $numTerms;
	}
	
	/**
	 * @return int
	 * @see TermDocs#getId
	 */
	public function getId()
	{
		return $this->docs->get($this->index);
	}
	
	/**
	 * @return int
	 * @see TermDocs#termCount
	 */
	public function termCount()
	{
		return $this->numTerms->get($this->index);
	}

	/**
	 * @return bool
	 * @see TermDocs#next
	 */
	public function next()
	{
		$this->index++;
		return ($this->docs->size() > $this->index);
	}

}

/**
 * {@link TermDocs}のデフォルト実装クラス	
 * 
 * @version 1.0 2009/04/03
 * @since Barge 1.3 2009/04/03
 * @see TermDocs
 */
class MatchAllDocs
{
	/**
	 * @var FileInputStream
	 */
	private $posInput;
	
	/**
	 * @var int
	 */
	private $doc;
	
	/**
	 * @var int
	 */
	private $maxDoc;
	
	/**
	 * @var int
	 */
	private $index = 0;
	
	/**
     * @param string $dir
	 */
	public function __construct(FileInputStream $input, $maxdoc)
	{
		$this->posInput = $input;
		$this->maxDoc = $maxdoc;
	}
	
	/**
	 * @return int
	 * @see TermDocs#getId
	 */
	public function getId()
	{
		return $this->doc;
	}
	
	/**
	 * @return int
	 * @see TermDocs#termCount
	 */
	public function termCount()
	{
		return $this->maxDoc;
	}

	/**
	 * @return bool
	 * @see TermDocs#next
	 */
	public function next()
	{
		$this->index++;
		$pos = (($this->index - 1) * 17) + 9;
		$this->posInput->seek($pos);
		$this->doc = intval($this->posInput->read(8));
		return ($this->maxDoc >= $this->index);
	}

}

/**
 * スコア計算のためのインターフェース	
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
interface Similarity
{
	/**
	 * フィールドに含まれるタームの数から重み付けを行う
	 * @param int $numTers フィールドに含まれる全ターム数	
	 * @return float
	 */
	public function lengthNorm($numTerms);
	
	/**
	 * @param int $freq フィールドに検索対象となるタームが出現する回数
	 * @return flaot 
	 */
    public function tf($freq);

}

/**
 * スコア計算のためのインターフェース	
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
class StandardSimilarity implements Similarity
{
	/**	
	 * @param int $numTers
	 * @return float
	 * @see Similarity#lengthNorm
	 */
	public function lengthNorm($numTerms)
	{
		// タームの数が多いほどヒットしやすくなる
		// ためタームが少ないほど高いスコアを与えるように設定する
		if ($numTerms <= 0)
			$numTerms = 1;
		return 1/Math::sqrt($numTerms);
	}
	
	/**
	 * @param int $freq	
	 * @return flaot 
	 */
    public function tf($freq)
    {
    	// 出現頻度が多ければ高いスコアを与える
        return sqrt($freq);
    }
}

/**
 * テキストのインデックス切り出しを行う単語解析インターフェース
 * 
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
interface Analyzer 
{
	/**	
	 * @param string $text テキスト	
	 * @return ArrayList
	 */
	public function analyze($text);
}

/**
 * 単語の解析結果
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
class Token 
{
	/**
	 * @var string
	 */
	private $text;
	
	/**
	 * @var int
	 */
	private $position = 0;
	
	/**
	 * @param string $text
	 * @param int $position 単語中にトークンが現れるオフセット、通常は単語中にトークンが
	 * 			出現する順番	
	 */
    public function __construct($text, $position)
    {
        $this->text = $text;
        $this->position = $position;
    }

    /**
     * @return string
     */
    public function getText()
    {
    	return $this->text;
    }
    
    /**
     * @return int 
     */
    public function getPosition()
    {
    	return $this->position;
    }
}


/**
 * 分ち書きアナライザー、日本語は２グラムで解析	
 * 
 * @since Barge 1.3 2009/04/03
 * @see Analyzer
 */
class StandardAnalyzer implements Analyzer
{
	/**
	 * @param string $text
	 * @return string 
	 */
	protected function filter($text)
	{
		// 英字を小文字に変換
		$text = strtolower($text);
		$text = mb_convert_kana($text, 'nasVKc', 'utf-8');
		return $text;
	}
	
	/**
	 * @param string $text
	 * @return ArrayList
	 */
	public function analyze($text)
	{
		$log = LogFactory::getLogger();
		$text = $this->filter($text);

		$tokens = new ArrayList();
		// 20--------------------------2f-~-3a----------40-~-5b--------60-~-7b--7d
		// ! " # $ % & ' ( ) * + , - . /    : ; < = > ? @    [ \ ] ^ _ `    { | } 
		$words = preg_split('/\s'
				.'|([\x20-\x2f])'
				.'|([\x3a-\x40])'
				.'|([\x5b-\x60])'
				.'|([\x7b-\x7d])'
				.'|(\xe3\x80[\x80-\x82])'
				.'|(\xef\xbd[\xa1-\xa5])/', 
				$text);
		foreach ($words as $i => $word) {
			if (mb_strlen($word) === 0) continue;	
			$tokens->add(new Token($word, $i));
		}
		return $tokens;
	}
}

/**
 * N-gram サポート
 * 
 * @since Barge 1.3 2009/04/03
 * @see Analyzer
 */
class BygramAnalyzer implements Analyzer
{
	/**
	 * @param string $text
	 * @return string 
	 */
	protected function filter($text)
	{
		// 英字を小文字に変換
		$text = strtolower($text);
		// n:「全角」数字を「半角」
		// a:「全角」英数字を「半角」
		// s:「全角」スペースを「半角」
		// s:「全角」スペースを「半角」
		// K:「半角カタカナ」を「全角カタカナ」
		// V：濁点の変換
		// K:「半角カタカナ」を「全角カタカナ」
		// c:「全角カタカナ」を「全角ひらがな」
		$text = mb_convert_kana($text, 'nasVKc', 'utf-8');
		return $text;
	}
	
	/**
	 * @param string $text
	 * @return ArrayList
	 */
	public function analyze($text)
	{
		$log = LogFactory::getLogger();
		$text = $this->filter($text);

		// 文節記号でリスト化しておく	
		$tokens = new ArrayList();
		$words = preg_split('/\s|([\x20-\x2f])|(\xe3\x80[\x80-\x82])|(\xef\xbd[\xa1-\xa5])/', $text);
		foreach ($words as $word) {
			$last = mb_strlen($word) - 1;
			if ($last < 1) {
				continue;
			}
			// 有効な文節に含まれる単語からＮグラムを作成	
			$i = 0;
			while ($i < $last) {
				$token = mb_substr($word, $i, 2);
				$log->debug("[{$token}]");
				$tokens->add(new Token($token, $i));
				$i++;	
			}
		}
		return $tokens;
	}
}



/**
 * トークンの列挙クラス	
 * 
 * @since Barge 1.3 2009/04/20
 */
class TermEnum
{
	/**
	 * @var array
	 */
	private $offsetCache = array();

	/**
	 * @var Logger
	 */
	private $log;
	
	/**
	 * タームの総数
	 * @var int
	 */
	private $termCount = 0;
	
	/**
	 * @var FileInputStream
	 */
	private $invInput;
	
	/**
	 * 現在のターム	
	 * @var Term
	 */
	private $term;
	
	/**
	 * 現在のタームが含まれるドキュメントの数	
	 * @var int
	 */
	private $docFreq;
	
	/**
	 * 現在のオフセット値
	 * @var int
	 */
	private $position = 0;
	
	/**
	 * @var FieldsInfo
	 */
	private $fieldsInfo;
	
	/**
	 * @param string $dir
	 * @param FieldsInfo $fieldsInfo
	 */
	public function __construct($dir, FieldsInfo $fieldsInfo)
	{
		$this->log = LogFactory::getLogger();
		$this->invInput = new FileInputStream($dir.'/.inv');
		$this->fieldsInfo = $fieldsInfo;
		$this->termCount = intval($this->invInput->read(8));
		
		// Seek first term.
		$this->next();
	}
	
	/**
	 * 
	 */
	public function close()
	{
		$this->invInput->close();
	}
	
	/**
	 * @param Term|string $term
	 */
	public function skipTo($term, $prefix = false)
	{
		if ($term instanceof Term) {
			return $this->seek($term->getText(), $term->getField(), $prefix);
		} else {
			return $this->seek((string)$term, null, $prefix); 
		}
	}
	
	/**
	 * 現在のタームが含まれるドキュメントの一覧を返す	
	 * 
	 * @return IndexTermDocs
	 */
	public function termDocs()
	{	
		$this->invInput->seek($this->positionedOffset($this->position));
		$temp = $this->invInput->read(29+3+8);			
		$stop = false;
		$slip = 5;
		$docs = new ArrayList();
		$numTerms = new ArrayList();
		while ($slip > 0) {
			$buff = $this->invInput->read(8+8+8);
			$docid = substr($buff, 0, 8);
//$this->log->debug('docid:'.$docid);
			if ($docid === '00000000') {
				$stop = true;
				break;
			}
//			substr($buff, 8, 3)
//			$fldid = intval(substr($buff, 8, 3));
//			if ($fieldNo !== $fldid) {
//				continue;
//			}
			$numTerms->add(intval(substr($buff, 8, 8)));
			$docs->add(intval($docid));
			$slip--;
		}
		while (!$stop) {
			$nextpos = $this->invInput->read(8);
//			if ($this->log->isDebugEnabled()) {
//				$this->log->debug('next : '.$nextpos);
//			}
			if ($nextpos !== '00000000') {
				$this->invInput->seek(intval($nextpos)+29+3+8);
				$slip = 5;
				while ($slip > 0) {
					$buff = $this->invInput->read(8+8+8);
					$docid = substr($buff, 0, 8);
					if ($docid === '00000000') {
						$stop = true;
						break;
					}
//					$fldid = intval(substr($buff, 8, 3));
//					if ($fieldNo !== $fldid) {
//						continue;
//					}
					$numTerms->add(intval(substr($buff, 8, 8)));
					$docs->add(intval($docid));
					$slip--;
				}
				// 改行
//				$this->invInput->read(1);
			} else {
				$stop = true;
			}
		}
//		$this->log->debug("Cost : {$cost}");
		return new IndexTermDocs($docs, $numTerms);
	}
	
	/**
	 * 現在のタームを返す	
	 * @return Term
	 */
	public function term()
	{
		return $this->term;
	}
	
	/**
	 * 現在のタームが含まれるドキュメント数を返す
	 * @return int
	 */
	public function docFreq()
	{
		return $this->docFreq;
	}
	
	/**
	 * @return bool
	 */
	public function next()
	{
		if ($this->position < $this->termCount) {
			$this->position++;
//			$this->log->debug('Next position : ' . $this->position);
			$this->invInput->seek($this->positionedOffset($this->position));
			$text = rtrim($this->invInput->read(29));
			$field = $this->fieldsInfo->getFieldName(intval($this->invInput->read(3)));
			$this->term = new Term($field, $text);
			$this->docFreq = intval($this->invInput->read(8));
			return true;
		} else {
			$this->term = null;
			return false;
		}
	}

	/**
	 * @param int $position
	 * @return int 
	 */
	protected function positionedOffset($position)
	{
		return (($position - 1) * 169) + 9;
	}
	
	/**
	 * @param string $token
	 * @param string $field
	 * @return string
	 */
	protected function termKey($token, $field = null)
	{
		return str_pad($token, 29, ' ').sprintf("%03d", $field ? $field : 0);
	}
	
	/**
	 * インデックスの指定されたトークンが出現する位置までシークします、プレフィックスサーチを指定した場合この
	 * メソッドは、指定されたトークンで最初に開始するターム位置へシークします　（完全一致するタームがある場合は
	 * その位置へ移動します。）
	 * 
	 * @param string $token トークン
	 * @param string $field フィールド名 
	 * @param bool $prefix 
	 * @return bool
	 */
	protected function seek($token, $field = null, $prefix = false)
	{
		$termkey = $this->termKey($token, $field);
		$searchFieldNo = $field !== null ? $this->fieldsInfo->getFieldNo($field) : -1;
		if (isset($this->offsetCache[$termkey])) {
			if ($this->offsetCache[$termkey] !== null) {
				$this->invInput->seek($this->positionedOffset($this->offsetCache[$termkey]['position']));
				$this->position = $this->offsetCache[$termkey]['position'];
				$this->term = $this->offsetCache[$termkey]['term'];
				$this->docFreq = $this->offsetCache[$termkey]['docFreq'];
				return true;
			} else {
				return false;
			}
		}
$this->log->debug('['.$token.']');
		// Binaly search
		$high = $this->termCount;
		$cost = 0;
		$low = 1;
		$match = false;
		$mid = 0;
		$prefixMatched = null;
		while ($high >= $low) {
			$mid = intval($low + (($high - $low) / 2));
			$this->position = $mid;
			$this->invInput->seek($this->positionedOffset($mid)); 
			$text = rtrim($this->invInput->read(29));			
			$fieldNo = intval($this->invInput->read(3));
//$this->log->debug($mid .'-'.$text);
//$this->log->debug('fieldno:'.$fieldNo);
			$this->docFreq = intval($this->invInput->read(8));
			// Save current term
			$this->term = new Term(
				$this->fieldsInfo->getFieldName($fieldNo), $text);
			if ($prefix) {
				if (String::startsWith($text, $token)) {
					$prefixMatched = array(
							'position'=>$mid, 'term'=>$this->term, 'docFreq'=>$this->docFreq);
				} 
			}
			// Compare searching term
			$ret = strcmp($text, $token);
$this->log->debug($token .'-'.$text);
			if ($ret === 0 && $field !== null) {
				$ret = strcmp(sprintf("%03d", $fieldNo), sprintf("%03d", $searchFieldNo)); 
			} 
			if ($ret > 0) {
				$high = $mid - 1;
			} else if ($ret < 0) {
				$low = $mid + 1;
			} else {
				$match = true;
				break;
			}
			$cost++;
		}
		
//		$this->log->debug("Search cost : {$cost}");
		if ($match) {
			$this->offsetCache[$termkey] = array(
				'position'=>$mid, 'term'=>$this->term, 'docFreq'=>$this->docFreq);
			return true;
		} else {
			// 完全一致しない場合は、最後にマッチした箇所に移動する
			if ($prefix && $prefixMatched !== null) {
				$this->invInput->seek($this->positionedOffset($prefixMatched['position']));
				$this->position = $prefixMatched['position'];
				$this->term = $prefixMatched['term'];
				$this->docFreq = $prefixMatched['docFreq'];
			} else {
				$this->offsetCache[$termkey] = null;
			}
			return false;
		}
	}
}


/**
 * インデックス情報を読み込むクラス
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
class IndexReader 
{
	/**
	 * @var string
	 */
	private $dir;
	
	/**
	 * @var FileInputStream
	 */
	private $dataInput;

	/**
	 * @var FileInputStream
	 */
	private $posInput;

	/**
	 * @var array
	 */
	private $invcache = array();

	/**
	 * フィールド定義	
	 * @var FieldsInfo
	 */
	private $fieldsInfo;

	/**
	 * @var TermEnum
	 */
	private $termEnum;
	
	/**
	 * @var bool
	 */
	private $closed = false;
	
	/**
	 * ドキュメントの総数	
	 * @var int
	 */
	private $maxDoc = 0;

	
	/**
	 * @param string $dir
	 */
	public function __construct($dir)
	{
		$this->dir = $dir;
		$this->fieldsInfo = new FieldsInfo($this->dir);
		$this->termEnum = new TermEnum($this->dir, $this->fieldsInfo);
		$this->posInput = new FileInputStream($this->dir.'/.pos');
		$this->maxDoc = intval($this->posInput->read(8));
	}

	/**
	 * 
	 * @param Term|string $term
	 * @return TermEnum
	 */
	public function termEnum($term = null, $prefix = false)
	{
		if ($this->closed) {
			$message = 'Index reader already closed';
			throw new Exception($message);
		}
		
		// FIXME : 引数なしの場合は先頭にポインターを移動	
		if ($term !== null) {
			$this->termEnum->skipTo($term);
		}
		return $this->termEnum;
	}
	
	/**
	 * 
	 * @param Term|string $term
	 * @return TermEnum
	 */
	public function prefixEnum($term = null)
	{
		if ($this->closed) {
			$message = 'Index reader already closed';
			throw new Exception($message);
		}
		
		// FIXME : 引数なしの場合は先頭にポインターを移動	
		if ($term !== null) {
			$this->termEnum->skipTo($term, true);
		}
		return $this->termEnum;
	}

	/**
	 * @return TermDocs
	 */
	public function docs()
	{
		return new MatchAllDocs($this->posInput, $this->maxDoc);
	}
	
	/**
	 * @param Term $term
	 * @return TermDocs
	 */
	public function termDocs(Term $term)
	{
		if ($this->closed) {
			$message = 'Index reader already closed';
			throw new Exception($message);
		}
		
		if ($this->termEnum->skipTo($term)) {
			return $this->termEnum->termDocs();
		} else {
			return new IndexTermDocs(new ArrayList(), new ArrayList());
		}
	}
	
	/**
	 * @param int $docid
	 * @return Document
	 * @throws Exception 無効なドキュメントＩＤ	
	 */
	public function getDocument($docid)
	{
		if ($this->closed) {
			$message = 'Index reader already closed';
			throw new Exception($message);
		}

		if ($this->dataInput === null)
			$this->dataInput = new FileInputStream($this->dir.'/.dat');
		
		$log = LogFactory::getLogger();
		
		$linesize = 17;
		$posit = 9 + (($docid - 1) * $linesize);
		if ($posit < 9) {
			throw new Exception("Invalidate document id : {$docid}");
		}

		$this->posInput->seek($posit);
		$line = $this->posInput->read($linesize);


		$offset = intval(substr($line, 8, 8));
//		$log->debug("Offset : {$offset}");
	
		$this->dataInput->seek($offset);
		
		$ndocid = intval($this->dataInput->read(8));
		$size = intval($this->dataInput->read(8));
		$body = $this->dataInput->read($size);

		if ($ndocid !== $docid) {
			$this->log->debug("Mismatched docuemnt id : {$ndocid}");
			throw new Exception("Invalidate document id : {$docid}");
		}
		$doc = new Document();
		$i = 0;
		while ($i < $size) {
			$fieldseq = intval(substr($body, $i, 3));
			$valueLength = intval(substr($body, $i + 3, 8));
			$field = new Field($this->fieldsInfo->getFieldName($fieldseq), substr($body, $i + 11, $valueLength));
			$doc->addField($field);
			$i += $valueLength + 3 + 8;
		}
		return $doc;
	}

	/**
	 * ドキュメント数
	 * @return int
	 */
	public function getMaxDoc()
	{
		return $this->maxDoc;
	}
	
	/**
	 */
	public function close()
	{
		if (!$this->closed) {
			$this->closed = true;
			if ($this->dataInput !== null)
				$this->dataInput->close();
			$this->posInput->close();
			$this->termEnum->close();
			$this->fieldsInfo->close();
		}
	}
}

interface Hits /*extends Countable, IteratorAggregate*/
{
}

//class QueryHits implements Hits
//{
//	/**
//	 * @var array
//	 */
//	private $docs;
//	
//	/**
//	 * @var array
//	 */
//	private $scores;
//	
//	/**
//	 * @var array
//	 */
//	private $index;
//	
//	/**
//	 * @param array $docs
//	 * @param array $scores
//	 */
//	public function __construct(array $docs, array $scores)
//	{
//		$this->docs = $docs;
//		$this->scores = $scores;
//		$this->index = array_combine($docs, $scores);
//	}
//	
//	public function intersect(QueryHits $hit)
//	{
//		foreach ($this->docs as $doc) {
//			return $this->docs; 
//		}
//	}
//
//}

///**
// * 検索ヒット情報
// * @version 1.0 2009/04/04
// * @since Barge 1.3 2009/04/03
// * @see Countable
// */
//abstract class Hits implements Countable, IteratorAggregate
//{
//	/**
//	 * @var IndexReader
//	 */
//	private $indexReader;
//	
//	/**
//	 * @var ArrayList
//	 */
//	private $hits;
//	
//	public function __construct(ArrayList $hits, IndexReader $indexReader)
//	{
//		$this->hits = $hits;
//		$this->indexReader = $indexReader;
//	}
//	
//	/**
//	 * @see Countable#count
//	 */
//	public function count()
//	{
//		return $this->hits->size();
//	} 
//	
//	/**
//	 * @return Iterator
//	 * @see IteratorAggregate#count
//	 */
//	public function getIterator()
//	{
//		return $this->hits->getIterator();
//	} 
//}

/**
 * 検索ヒット情報
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
class Hit 
{
	/**
	 * @var float
	 */
	private $score = 1.0;
	
	/**
	 * @var int
	 */
	private $id;
	
	/**
	 * @var IndexReader 
	 */
	private $indexReader;
	
	/**
	 * @var Document
	 */
	private $document;

	/**
	 * @param int $id
	 * @param IndexReader $indexReader
	 */
	public function __construct($id, IndexReader $indexReader)
	{
		$this->id = $id;
		$this->indexReader = $indexReader;
	}
	
	/**
	 * @return int
	 */
	public function getId()
	{
		return $this->id;
	}
	
	/**
	 * @return Document
	 */
	public function getDocument()
	{
		if ($this->document === null) {
			$this->document = $this->indexReader->getDocument($this->id);
		}
		return $this->document;
	}
	
	/**
	 * @param float $score
	 */
	public function setScore($score)
	{
		$this->score = $score;
	}

	/**
	 * @return flaot
	 */
	public function getScore()
	{
		return $this->score;
	}
	
//	/**
//	 * @return string 
//	 */
//	public function __toString()
//	{
//		return (string)$this->id;
//	}
}

/**
 * Query interface.
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
interface Query
{
	/**
	 * @param IndexReader $reader
	 * @param Similarity $similarity
	 * @return ArrayList <Hit>
	 */
	public function execute(IndexReader $reader, Similarity $similarity);
	
	/**
	 * @param int $boost
	 */
	public function setBoost($boost);
}
//
///**
// * 
// * @version 1.0 2009/04/04
// * @since Barge 1.3 2009/04/03
// * @see Query
// */
//class MultiTermQuery implements Query
//{
//	/**
//	 * @var string
//	 */
//	private $field;
//	
//	/**
//	 * @var array
//	 */
//	private $terms;
//	
//	/**
//	 * @var int
//	 */
//	private $boost = 1.0;
//	
//	/**
//	 * @param string $field
//	 * @param array $terms
//	 */
//	public function __construct($field, $terms = array())
//	{
//		$this->field = $field;
//		$this->terms = $terms;
//	}
//	
//	/**
//	 * @param int $boost
//	 */
//	public function setBoost($boost)
//	{
//		$this->boost = $boost;
//	}
//	
//	/**
//	 * @param Term $term
//	 */
//	public function addTerm(Term $term)
//	{
//		$this->terms[] = $term;
//	}
//	
//	/**
//	 * @param IndexReader $reader
//	 * @return ArrayList
//	 * @see Query#execute
//	 */
//	public function execute(IndexReader $reader, Similarity $similarity)
//	{
//		$log = LogFactory::getLogger();
//		$hits = new HashMap();
////		// フィールドの boost 値を取得
////		$fieldBoost = isset($this->boosts[$this->field]) ? $this->boosts[$this->field] : 1.0;
//
//		foreach ($this->terms as $term) {
//			/* @var $term Term */
////			$log->debug($term->getField().'='.$term->getText());
//			$termDocs = $reader->termDocs($term);
//			while ($termDocs->next()) {	
//				$hit = new Hit($termDocs->getId(), $reader);
//				$hit->setScore($this->boost);	
//				if ($hits->contains($hit->getId())) {
//					$oldhit = $hits->get($hit->getId());
//					$oldhit->setScore($oldhit->getScore() + $hit->getScore());
//				} else {
//					$hits->put($hit->getId(), $hit);
//				}
//			}
//		}
//	
//		return $hits->values();
////		return new Hits();
//	}
//}

/**
 * 単語を検索するためのクエリー
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 * @see Query
 */
class TermQuery implements Query
{
	/**
	 * @var Term
	 */
	private $term;
	
	/**
	 * @var int
	 */
	private $boost = 1.0;
	
	/**
	 * @param Term $term
	 */
	public function __construct(Term $term)
	{
		$this->term = $term;
	}
	
	/**
	 * @param float $boost
	 */
	public function setBoost($boost)
	{
		$this->boost = $boost;
	}
	
	/**
	 * @param IndexReader $indexReader
	 * @param Similarity $similarity
	 * @return ArrayList
	 * @see Query#execute
	 */
	public function execute(IndexReader $indexReader, Similarity $similarity)
	{
		$log = LogFactory::getLogger();
		$hits = array();
		$freqs = array();

		/* @var $term Term */
//		$log->debug("-> term ". $this->term->getField()."=".$this->term->getText());
		
		$termDocs = $indexReader->termDocs($this->term);
		while ($termDocs->next()) {
			$doc = $termDocs->getId();

			// 希少率
			$idf = 1;	
			// 出現率
			$tf = $similarity->tf(1);
			// ヒット可能性
			$fieldNorm = $similarity->lengthNorm($termDocs->termCount());
				
			if (isset($hits[$doc])) {
				$freqs[$doc]++;
				
				// 出現率を加算してスコアを再計算	
				$tf = $similarity->tf($freqs[$doc]);
				$hits[$doc]->setScore($fieldNorm * $tf * $this->boost);
//				$oldscore = $hits[$doc]->getScore();
//				$hit->setScore($oldscore * );
//				$hit->setScore($hits[$doc]->getScore() + $hit->getScore());
			} else {
				// FIXME : 計算したものを格納しておくこと可能
	//LogFactory::getLogger()->debug($termDocs->termCount());

				$hit = new Hit($termDocs->getId(), $indexReader);
				$hit->setScore($fieldNorm * $tf * $this->boost);
				
				$freqs[$doc] = 1;
				$hits[$doc] = $hit;
			}
		}

//		var_dump($hits);exit(0);
//$log->debug('Hit : ' .count($hits));
//$log->debug('Hit : ' .count($hits->values()));
		return new ArrayList($hits);
	}
}

/**
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 * @see Query
 */
class EmptyQuery implements Query
{	
	/**
	 * @param int $boost
	 */
	public function setBoost($boost)
	{
		// Do nothing.
	}
	
	/**
	 * @param IndexReader $reader
	 * @return ArrayList
	 * @see Query#execute
	 */
	public function execute(IndexReader $reader, Similarity $similarity)
	{
		return new ArrayList();
	}
}

/**
 * 論理演算（and, or ）を行うためのクエリー
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 * @see Query
 */
class BooleanQuery implements Query
{
	/**
	 * AND 結合
	 */
	const MUST = 1;
	
	/**
	 * OR 結合
	 */
	const SHOULD = 2;

	/**
	 * @var array
	 */
	private $queries = array();
	
	/**
	 * @var int
	 */
	private $boost = 1.0;
	
	/**
	 * @param float $boost
	 */
	public function setBoost($boost)
	{
		$this->boost = $boost;
	}
	
	/**
	 * @param Query $query
	 * @param int $flag
	 */
	public function addQuery(Query $query, $flag = self::SHOULD)
	{
		$this->queries[] = array(
			'query'=>$query, 
			'flag'=>$flag
			);
	}
	
	/**
	 * @param IndexReader $reader
	 * @return ArrayList
	 * @see Query#execute
	 */
	public function execute(IndexReader $reader, Similarity $similarity)
	{
		$first = true;
		$hits = new ArrayList();
		$map = new HashMap();
		foreach ($this->queries as $combine) {	/* @var $query Query */
			$query = $combine['query'];
			$flag = $combine['flag'];
			if ($first) {
				$hits = $query->execute($reader, $similarity);
				// Create map
				foreach ($hits as $hit)	{/* @var $hit Hit */
//					LogFactory::getLogger()->debug('** First **');
//					LogFactory::getLogger()->debug($hit->getId());
					$map->put($hit->getId(), $hit);
				}

			} else {
				$hits2 = $query->execute($reader, $similarity);

				if ($flag === self::MUST) {
					// TODO : この処理は重い	
					$merged = new HashMap();
					foreach ($hits2 as $hit) {	/* @var $hit Hit */
						if ($map->contains($hit->getId())) {
							$base = $map->get($hit->getId());
							$base->setScore($base->getScore() + $hit->getScore());
							$merged->put($base->getId(), $base);
						}
					}
					$map = $merged;
				} else if ($flag === self::SHOULD) {
					// TODO : この処理は重い	
					foreach ($hits2 as $hit) {	/* @var $hit Hit */
						if ($map->contains($hit->getId())) {
							$base = $map->get($hit->getId());
							$base->setScore($base->getScore() + $hit->getScore());
						} else {
							$map->put($hit->getId(), $hit);
						}
					}
//					$map = $merged;
				}

			}
			$first = false;
		}
		// TODO : スコアの再計算
		return $map->values();
	}
}

/**
 * @version 1.0 2009/05/06
 * @since barge 1.3 2009/05/06
 * @see Query 
 */
class MatchAllDocsQuery implements Query 
{
	/**
	 * @param IndexReader $reader
	 * @param Similarity $similarity
	 * @return ArrayList <Hit>
	 */
	public function execute(IndexReader $reader, Similarity $similarity)
	{
		$hits = array();
		$termDocs = $reader->docs();
		while ($termDocs->next()) {
			$hit = new Hit($termDocs->getId(), $reader);
			$hit->setScore(1.0);
			$hits[] = $hit; 
		}
		return new ArrayList($hits);
	}
	
	/**
	 * @param int $boost
	 */
	public function setBoost($boost)
	{
		// Do nothing.
	}
}

/**
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
class SortField
{
	private $field;
	private $reverse = false;
	
	/**
	 * @param string $field
	 * @param bool $reverse
	 */
	public function __construct($field, $reverse = false)
	{
		$this->field = $field;
		$this->reverse = $reverse;
	}

	/**
	 * @return array
	 */
	public function isReverse()
	{
		return $this->field;
	}
	
	/**
	 * @return array
	 */
	public function getField()
	{
		return $this->field;
	}
}


/**
 * インデックス検索クラス
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
class QueryParser 
{
	/** @var Analyzer */
	private $analyzer;
	
	/**
	 * @param Analyzer $analyzer
	 */
	public function __construct(Analyzer $analyzer)
	{
		$this->analyzer = $analyzer;
	}
	
	/**
	 *
	 * @param string $field
	 * @param string $text
	 * @param float $boost
	 * @return Query
	 */
	public function parse($field, $text, $boost = 1.0)
	{
		$tokens = $this->analyzer->analyze($text);
		$query = null;
		if ($tokens->isEmpty()) {
			$query = new EmptyQuery();
		} else if ($tokens->size() === 1) {
			$query = new TermQuery(new Term($field, $tokens->get(0)->getText()));
		} else {
			$query = new BooleanQuery();
			foreach ($tokens as $token) {	/* @var $token Token */
				$query->addQuery(new TermQuery(new Term($field, $token->getText())));
			}
		}
		$query->setBoost($boost);
		return $query;
	}
}

/**
 * インデックス検索クラス
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
class IndexSearcher 
{
	var $dir;
	
	/**
	 * @var Logger
	 */
	private $log;
	
	/**
	 * @var IndexReader
	 */
	private $indexReader;
	
	/**
	 * @var Analyzer
	 */
	private $analyzer;
	
	/**
	 * @var array
	 */
	private $boosts = array();
	
	/**
	 * @var Similarity
	 */
	private $similarity;
	
	/**
	 * @param string $dir
	 * @param Analyzer $analyzer
	 * @param Similarity $similarity
	 */
	public function __construct($dir, Analyzer $analyzer, Similarity $similarity = null)
	{
		$this->log = LogFactory::getLogger();
		$this->dir = $dir;
		$this->indexReader = new IndexReader($dir);
		$this->analyzer = $analyzer;
		if ($similarity !== null) {
			$this->similarity = $similarity;
		} else { 
			$this->similarity = new StandardSimilarity();
		}
	}
	
	/**
	 * フィールドに対するブースト値を設定する、ブースト値を設定することで
	 * 特定のフィールド（例えばタイトルなど）にヒットしたドキュメントのスコア
	 * を強調するといったことが可能になります。
	 *
	 * @param string $field
	 * @param float $boost 指定しない場合は 1.0 が設定されています
	 */
	public function setBoost($field, $boost )
	{
		$this->boosts[$field] = $boost;
	}
	
//	/**
//	 * @param int $docId ドキュメントＩＤ
//	 * @return Document
//	 */
//	public function getDocument($docId)
//	{
//		return $this->indexReader->getDocument($docId);
//	}
	
	/**
	 * @param Query $query
	 * @param Sort $sort
	 * @return ArrayList
	 */
	public function search(Query $query, Sort $sort = null)
	{
		// TODO : ソート条件が指定されている場合はスコア計算は不要	
		$hits = $query->execute($this->indexReader, $this->similarity);
		if ($sort !== null) 
			$hits = $sort->sort($hits, $this->indexReader);
		return $hits;
	}

	/**
	 * @param Similarity $similarity
	 */
	public function setSimilarity(Similarity $similarity)
	{
		$this->similarity = $similarity;
	}

	/**
	 * @since 1.0 2009/04/25
	 */
	public function close()
	{
		$this->indexReader->close();
	}
}

/**
 * ソートインターフェース
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 */
interface Sort
{
	/**
	 * @param ArrayList $hits
	 * @param IndexReader $indexReader
	 */
	public function sort(ArrayList $hits, IndexReader $indexReader);
}

/**
 * 検索スコアでのソート
 * 
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 * @see Sort
 * @see Comparator
 */
class ScoreSort implements Sort, Comparator 
{
	/**
	 * @param ArrayList $hits
	 * @param IndexReader $reader
	 * @return ArrayList 
	 */
	public function sort(ArrayList $hits, IndexReader $reader)
	{
		return $hits->sort($this);
	}
	
	/**
	 * @param Hit $a
	 * @param Hit $b
	 * @return int
	 */
	public function compare($a, $b)
	{
		return ($a->getScore() < $b->getScore() ? 1 : -1);
	}
}

/**
 * フィールド値でのソート
 * @version 1.0 2009/04/04
 * @since Barge 1.3 2009/04/03
 * @see Sorter
 */
class FieldSort implements Sort, Comparator 
{
	/**
	 * @var array
	 */
	private $fields = array();
	
	/**
	 * @var IndexReader
	 */
	private $reader;
	
	/**
	 * @var Logger
	 */
	private $log;
	
	/**
	 * @param ArrayList $hits
	 * @param IndexReader $reader
	 * @return ArrayList 
	 */
	public function sort(ArrayList $hits, IndexReader $reader)
	{
		$this->log = LogFactory::getLogger();
		$this->reader = $reader;
		return $hits->sort($this);
	}

	/**
	 * @param array $hits
	 */
	public function addField($field, $reverse = false)
	{
		$this->fields[] = array(
			'field' => $field,
			'reverse' => $reverse,
			);
	}
	
	/**
	 * @param Hit $a
	 * @param Hit $b
	 * @return int
	 */
	public function compare($hit1, $hit2)
	{
		/* @var $hit1 Hit */
		$doca = $hit1->getDocument();
		/* @var $hit2 Hit */
		$docb = $hit2->getDocument();
		$ret = 0;
		foreach ($this->fields as $arary) {
			$field = $arary['field'];
			$reverse = $arary['reverse'];
			$ret = strcmp($doca->getFieldValue($field), 
				$docb->getFieldValue($field));
			if ($ret !== 0) {
				$ret = $reverse ? ($ret * -1) : $ret;  
				break;
			}
		}
		return ($ret);
	}
}

